# -*- coding: utf-8 -*-
"""
gurobi_gas_opt_full.py
---------------------------------
带“算例选择（预设/JSON）+ 甘特图”的 Gurobi MILP 验证脚本：
- 工艺：清洗 → 充装（同瓶串行 & 底气最后）→ 摇匀 → 分析（分组并测、同步起止、组时长 = 组内最大分析时长）
- 分析设备：显式能力矩阵 can[d][c]，并对设备时间线做不重叠
- 目标：最小化总完工时间（Makespan）
- 支持两种 JSON 格式，并兼容 num_components=2 时 inflation_time 给成一维 [B] 的情况

用法：
1) 交互式选择：python gurobi_gas_opt_full.py
2) 指定预设：    python gurobi_gas_opt_full.py --preset small --time_limit 60 --seed 42
3) 指定 JSON：   python gurobi_gas_opt_full.py --json ./instances/user_case.json --time_limit 60

依赖：gurobipy、matplotlib（仅用于甘特图）
"""

import argparse
from typing import List, Dict
import itertools

try:
    import gurobipy as gp
    from gurobipy import GRB
except Exception as e:
    raise RuntimeError("请先安装 Gurobi 并配置许可（pip install gurobipy）。当前错误：%s" % str(e))


# ------------------------- 工具：随机可复现 -------------------------
def set_seed(seed: int = 42):
    import random, numpy as np
    random.seed(seed)
    np.random.seed(seed)


# ------------------------- 预设算例生成（能力矩阵风格） -------------------------
def build_preset_instance(preset: str = "small", seed: int = 42) -> Dict:
    """
    small / medium / large 的内置示例（能力矩阵风格）。
    注意：large 可能较慢，建议先在 small / medium 上验证。
    """
    set_seed(seed)
    preset = preset.lower()
    assert preset in {"small", "medium", "large"}

    import numpy as np

    if preset == "small":
        B = 4
        components = ["A","B","C","D","N2"]; C = len(components); bottom = C-1
        F = 1; CL = 1; SK = 1; Gmax = 3
        # 分析设备能力：3 台相邻覆盖
        Dv = 3
        can = [
            [1,1,0,0,0],  # D0: A,B
            [0,1,1,0,0],  # D1: B,C
            [0,0,1,1,0],  # D2: C,D
        ]
        anal_time = [
            [3,2,0,0,0],
            [0,2,1,0,0],
            [0,0,2,3,0],
        ]
        clean_time = [np.random.randint(4,7) for _ in range(B)]
        shake_time = [np.random.randint(2,4) for _ in range(B)]
        fill_time = []
        for b in range(B):
            row = [np.random.randint(5,10) for _ in range(C-1)] + [np.random.randint(3,6)]  # N2
            # 随机去除少量不需要的组分（20% 概率置 0）
            for c in range(C-1):
                if np.random.rand() < 0.2:
                    row[c] = 0
            fill_time.append(row)

    elif preset == "medium":
        B = 10
        components = ["A","B","C","D","E","N2"]; C = len(components); bottom = C-1
        F = 2; CL = 1; SK = 1; Gmax = 3
        Dv = 5
        can = [
            [1,1,0,0,0,0],
            [0,1,1,0,0,0],
            [0,0,1,1,0,0],
            [1,0,0,1,0,0],
            [0,0,0,1,1,0],
        ]
        anal_time = [[0]*C for _ in range(Dv)]
        import numpy as np
        for d in range(Dv):
            for c in range(C):
                if can[d][c]==1:
                    anal_time[d][c] = int(np.random.randint(1,4))
        clean_time = [int(np.random.randint(4,8)) for _ in range(B)]
        shake_time = [int(np.random.randint(2,5)) for _ in range(B)]
        fill_time = []
        for b in range(B):
            row = [int(np.random.randint(6,12)) for _ in range(C-1)] + [int(np.random.randint(3,7))]
            for c in range(C-1):
                if np.random.rand() < 0.25:
                    row[c] = 0
            fill_time.append(row)

    else:  # large
        B = 20
        components = ["A","B","C","D","E","F","N2"]; C = len(components); bottom = C-1
        F = 3; CL = 1; SK = 1; Gmax = 3
        Dv = 6
        can = [
            [1,1,0,0,0,0,0],
            [0,1,1,0,0,0,0],
            [0,0,1,1,0,0,0],
            [0,0,0,1,1,0,0],
            [1,0,0,0,0,1,0],
            [0,0,1,0,1,0,0],
        ]
        anal_time = [[0]*C for _ in range(Dv)]
        import numpy as np
        for d in range(Dv):
            for c in range(C):
                if can[d][c]==1:
                    anal_time[d][c] = int(np.random.randint(1,5))
        clean_time = [int(np.random.randint(5,9)) for _ in range(B)]
        shake_time = [int(np.random.randint(3,6)) for _ in range(B)]
        fill_time = []
        for b in range(B):
            row = [int(np.random.randint(6,12)) for _ in range(C-1)] + [int(np.random.randint(3,7))]
            for c in range(C-1):
                if np.random.rand() < 0.3:
                    row[c] = 0
            fill_time.append(row)

    return dict(B=B, C=len(components), D=Dv, F=F, CL=CL, SK=SK, Gmax=Gmax,
                components=components, bottom=bottom,
                clean_time=clean_time, fill_time=fill_time, shake_time=shake_time,
                can=can, anal_time=anal_time)


# ------------------------- JSON 读取/转换 -------------------------
def load_instance_from_json(path: str) -> Dict:
    """
    支持两种 JSON 风格：
    A) 能力矩阵风格（推荐，最真实）：含 components, can, anal_time
    B) 旧脚本风格（per-component 台数）：含 problem_type=gas_scheduling, parameters, processing_times
       且兼容 num_components=2 时 inflation_time 给成一维 [B] 的情况
    """
    import json, numpy as np
    with open(path, 'r', encoding='utf-8') as f:
        data = json.load(f)

    # ---- 风格 A：能力矩阵风格 ----
    if 'components' in data and 'can' in data and 'anal_time' in data:
        components = data['components']
        C = len(components)
        bottom = int(data.get('bottom', components.index('N2') if 'N2' in components else C-1))
        return dict(
            B=int(data['B']), C=C, D=int(data['D']), F=int(data['F']),
            CL=int(data.get('CL', 1)), SK=int(data.get('SK', 1)),
            Gmax=int(data.get('Gmax', 3)),
            components=components, bottom=bottom,
            clean_time=[float(x) for x in data['clean_time']],
            fill_time=[[float(x) for x in row] for row in data['fill_time']],
            shake_time=[float(x) for x in data['shake_time']],
            can=[[int(v) for v in row] for row in data['can']],
            anal_time=[[float(x) for x in row] for row in data['anal_time']],
        )

    # ---- 风格 B：旧脚本风格 ----
    if data.get('problem_type') == 'gas_scheduling' and 'parameters' in data and 'processing_times' in data:
        P = data['parameters']; T = data['processing_times']
        B = int(P['num_workpieces']); C = int(P['num_components'])
        components = data.get('components', [chr(ord('A')+i) for i in range(C-1)] + ['N2'])
        bottom = components.index('N2') if 'N2' in components else C-1
        F = int(P.get('num_inflation_eq', 1))
        CL = int(P.get('num_clean_eq', 1))
        SK = int(P.get('num_shake_eq', 1))
        Gmax = int(P.get('Gmax', 3))

        # 充装时间：允许 [B][C] 或 [B]（当 C==2，视为 [非底气, N2]，N2 时间置 0）
        raw_fill = T['inflation_time']
        if isinstance(raw_fill, list) and raw_fill and isinstance(raw_fill[0], list):
            fill_time = raw_fill
        elif isinstance(raw_fill, list):
            if C != 2:
                raise ValueError("检测到 inflation_time 为一维 [B]，但 num_components != 2，无法自动展开。请提供二维 [B][C]。")
            fill_time = [[float(t), 0.0] for t in raw_fill]
        else:
            raise ValueError("无法识别 processing_times.inflation_time 的结构，请提供 [B] 或 [B][C]。")

        # 旧风格没有明确的设备能力矩阵：用“每个非底气组分 k 台专用设备”近似（底气不分析）
        k = int(P.get('num_analysis_eq_per_component', 1))
        Dv = k * (C-1)
        can = [[0]*C for _ in range(Dv)]
        anal_time = [[0.0]*C for _ in range(Dv)]

        # 若给了 [B][C] 的 analysis_time，我们取各组分的中位数作为设备时长
        anal_by_comp = None
        if 'analysis_time' in T:
            anal_by_comp = np.array(T['analysis_time'], dtype=float)  # [B][C]
            med = [float(np.median(anal_by_comp[:, c])) for c in range(C)]
        else:
            med = [2.0]*C  # 默认

        d = 0
        for c in range(C-1):  # 不含底气
            for _ in range(k):
                can[d][c] = 1
                anal_time[d][c] = med[c] if med[c] > 0 else 2.0
                d += 1

        # 清洗和摇匀时间
        if isinstance(T.get('clean_time'), list):
            clean_time = [float(x) for x in T['clean_time']]
        else:
            clean_time = [float(T.get('clean_time', 5.0)) for _ in range(B)]
        if isinstance(T.get('shake_unit_time'), list):
            shake_time = [float(x) for x in T['shake_unit_time']]
        else:
            shake_time = [float(T.get('shake_unit_time', 3.0)) for _ in range(B)]

        return dict(B=B, C=C, D=Dv, F=F, CL=CL, SK=SK, Gmax=Gmax,
                    components=components, bottom=bottom,
                    clean_time=clean_time, fill_time=fill_time, shake_time=shake_time,
                    can=can, anal_time=anal_time)

    raise ValueError("无法识别的 JSON 结构，请参考脚本顶部注释中的两种格式。")


# ------------------------- MILP 核心 -------------------------
def build_and_solve(inst: Dict, time_limit: int = 60, mip_gap: float = 1e-4):
    """
    Gurobi 模型：
    - 清洗：单机不重叠
    - 充装：支持多台充装机；同瓶串行；底气最后；同设备不重叠
    - 摇匀：单机不重叠；在充装完成后
    - 分析：分组并测 + 同步起止 + 设备能力矩阵 + 同设备不重叠；同瓶不同组不重叠
    目标：最小化 Makespan
    """
    B = inst['B']
    C = inst['C']
    Dv = inst['D']
    F = inst['F']
    CL = inst['CL']
    SK = inst['SK']
    Gmax = inst['Gmax']
    components = inst['components']
    bottom = inst['bottom']

    clean_time: List[float] = inst['clean_time']
    fill_time: List[List[float]] = inst['fill_time']
    shake_time: List[float] = inst['shake_time']
    can: List[List[int]] = inst['can']
    anal_time: List[List[float]] = inst['anal_time']

    # 上界 M（保守）
    T_clean = sum(clean_time)
    T_fill = sum(sum(max(ft, 0.0) for ft in row) for row in fill_time)
    T_shake = sum(shake_time)
    T_anal = 0.0
    for b in range(B):
        for c in range(C-1):
            best = max(anal_time[d][c] if can[d][c] else 0.0 for d in range(Dv))
            T_anal += best
    M = 10.0 * (T_clean + T_fill + T_shake + T_anal + 1.0)

    m = gp.Model("GasMixShop_MILP")

    # --- 时间变量 ---
    S_clean = m.addVars(B, vtype=GRB.CONTINUOUS, lb=0.0, name="S_clean")
    E_clean = m.addVars(B, vtype=GRB.CONTINUOUS, lb=0.0, name="E_clean")

    S_fill = m.addVars(B, C, vtype=GRB.CONTINUOUS, lb=0.0, name="S_fill")
    E_fill = m.addVars(B, C, vtype=GRB.CONTINUOUS, lb=0.0, name="E_fill")
    E_fill_last = m.addVars(B, vtype=GRB.CONTINUOUS, lb=0.0, name="E_fill_last")

    S_shake = m.addVars(B, vtype=GRB.CONTINUOUS, lb=0.0, name="S_shake")
    E_shake = m.addVars(B, vtype=GRB.CONTINUOUS, lb=0.0, name="E_shake")

    S_grp = m.addVars(B, Gmax, vtype=GRB.CONTINUOUS, lb=0.0, name="S_grp")
    E_grp = m.addVars(B, Gmax, vtype=GRB.CONTINUOUS, lb=0.0, name="E_grp")
    T_grp = m.addVars(B, Gmax, vtype=GRB.CONTINUOUS, lb=0.0, name="T_grp")

    C_b = m.addVars(B, vtype=GRB.CONTINUOUS, lb=0.0, name="C_b")
    MKS = m.addVar(vtype=GRB.CONTINUOUS, lb=0.0, name="Makespan")

    # --- 指派与不重叠 ---
    o_clean = m.addVars(B, B, vtype=GRB.BINARY, name="ord_clean")
    o_shake = m.addVars(B, B, vtype=GRB.BINARY, name="ord_shake")

    a_fill = m.addVars(B, C, F, vtype=GRB.BINARY, name="a_fill")
    u_fill = m.addVars(B, C, B, C, F, vtype=GRB.BINARY, name="u_fill")
    k_serial = m.addVars(B, C, C, vtype=GRB.BINARY, name="serial_fill")

    v_grp = m.addVars(B, Gmax, vtype=GRB.BINARY, name="v_grp")
    z = m.addVars(B, C-1, Gmax, vtype=GRB.BINARY, name="z")
    x = m.addVars(B, C-1, Gmax, Dv, vtype=GRB.BINARY, name="x")
    y = m.addVars(B, Gmax, Dv, vtype=GRB.BINARY, name="y")

    o_dev = m.addVars(B, Gmax, B, Gmax, Dv, vtype=GRB.BINARY, name="ord_dev")
    o_grp = m.addVars(B, Gmax, Gmax, vtype=GRB.BINARY, name="ord_grp")

    # --- 清洗阶段 ---
    for b in range(B):
        m.addConstr(E_clean[b] == S_clean[b] + clean_time[b], name=f"C_clean_dur[{b}]")
    for b in range(B):
        for bp in range(B):
            if b == bp: continue
            m.addConstr(S_clean[bp] >= E_clean[b] - M * (1 - o_clean[b, bp]), name=f"C_clean_seq1[{b},{bp}]")
            m.addConstr(S_clean[b]  >= E_clean[bp] - M * (o_clean[b, bp]),  name=f"C_clean_seq2[{b},{bp}]")

    # --- 充装阶段 ---
    for b in range(B):
        for c in range(C):
            m.addConstr(E_fill[b, c] == S_fill[b, c] + fill_time[b][c], name=f"C_fill_dur[{b},{c}]")
            m.addConstr(S_fill[b, c] >= E_clean[b], name=f"C_fill_after_clean[{b},{c}]")
            m.addConstr(gp.quicksum(a_fill[b, c, f] for f in range(F)) == 1, name=f"C_fill_assign[{b},{c}]")

    # 同一设备上的不重叠（条件：两工序都在该设备）
    for f in range(F):
        for b, c, bp, cp in itertools.product(range(B), range(C), range(B), range(C)):
            if (b, c) >= (bp, cp):
                continue
            # (b,c) 在 (bp,cp) 之前
            m.addConstr(
                S_fill[bp, cp] >= E_fill[b, c]
                - M * (1 - u_fill[b, c, bp, cp, f])
                - M * (1 - a_fill[b, c, f])
                - M * (1 - a_fill[bp, cp, f]),
                name=f"C_fill_noovl1[{b},{c},{bp},{cp},{f}]"
            )
            # (bp,cp) 在 (b,c) 之前
            m.addConstr(
                S_fill[b, c] >= E_fill[bp, cp]
                - M * (u_fill[b, c, bp, cp, f])
                - M * (1 - a_fill[b, c, f])
                - M * (1 - a_fill[bp, cp, f]),
                name=f"C_fill_noovl2[{b},{c},{bp},{cp},{f}]"
            )

    # 同瓶充装串行（不允许并行）+ 底气最后
    for b in range(B):
        for c in range(C):
            for cp in range(C):
                if c == cp:
                    continue
                m.addConstr(S_fill[b, cp] >= E_fill[b, c] - M * (1 - k_serial[b, c, cp]),
                            name=f"C_fill_serial1[{b},{c},{cp}]")
                m.addConstr(S_fill[b, c]  >= E_fill[b, cp] - M * (k_serial[b, c, cp]),
                            name=f"C_fill_serial2[{b},{c},{cp}]")
        for c in range(C-1):
            m.addConstr(E_fill[b, c] <= S_fill[b, bottom], name=f"C_fill_bottom_last[{b},{c}]")

    for b in range(B):
        for c in range(C):
            m.addConstr(E_fill_last[b] >= E_fill[b, c], name=f"C_fill_lastmax[{b},{c}]")

    # --- 摇匀阶段（单机） ---
    for b in range(B):
        m.addConstr(E_shake[b] == S_shake[b] + shake_time[b], name=f"C_shake_dur[{b}]")
        m.addConstr(S_shake[b] >= E_fill_last[b], name=f"C_shake_after_fill[{b}]")
    for b in range(B):
        for bp in range(B):
            if b == bp: continue
            m.addConstr(S_shake[bp] >= E_shake[b] - M * (1 - o_shake[b, bp]), name=f"C_shake_noovl1[{b},{bp}]")
            m.addConstr(S_shake[b]  >= E_shake[bp] - M * (o_shake[b, bp]),  name=f"C_shake_noovl2[{b},{bp}]")

    # --- 分析阶段：分组并测 + 同步起止 + 设备能力 ---
    for b in range(B):
        for c in range(C-1):
            m.addConstr(gp.quicksum(z[b, c, g] for g in range(Gmax)) == 1, name=f"C_group_cover[{b},{c}]")
        for g in range(Gmax):
            for c in range(C-1):
                m.addConstr(v_grp[b, g] >= z[b, c, g], name=f"C_group_exist_lb[{b},{g},{c}]")
            m.addConstr(v_grp[b, g] <= gp.quicksum(z[b, c, g] for c in range(C-1)), name=f"C_group_exist_ub[{b},{g}]")

    for b in range(B):
        for c in range(C-1):
            for g in range(Gmax):
                m.addConstr(gp.quicksum(x[b, c, g, d] for d in range(Dv) if can[d][c]==1) == z[b, c, g],
                            name=f"C_group_device_choose[{b},{c},{g}]")
                for d in range(Dv):
                    if can[d][c]==0:
                        m.addConstr(x[b, c, g, d] == 0, name=f"C_forbid_incap[{b},{c},{g},{d}]")

    for b in range(B):
        for g in range(Gmax):
            for d in range(Dv):
                m.addConstr(gp.quicksum(x[b, c, g, d] for c in range(C-1)) <= 1, name=f"C_one_comp_per_dev[{b},{g},{d}]")
                m.addConstr(y[b, g, d] == gp.quicksum(x[b, c, g, d] for c in range(C-1)), name=f"C_y_def[{b},{g},{d}]")

    for b in range(B):
        for g in range(Gmax):
            m.addConstr(T_grp[b, g] <= M * v_grp[b, g], name=f"C_Tgrp_zero_if_empty[{b},{g}]")
            for c in range(C-1):
                for d in range(Dv):
                    if can[d][c]==1:
                        m.addConstr(T_grp[b, g] >= anal_time[d][c] * x[b, c, g, d],
                                    name=f"C_Tgrp_maxlb[{b},{g},{c},{d}]")
            m.addConstr(E_grp[b, g] == S_grp[b, g] + T_grp[b, g], name=f"C_grp_dur[{b},{g}]")
            m.addConstr(S_grp[b, g] >= E_shake[b] - M * (1 - v_grp[b, g]), name=f"C_grp_after_shake[{b},{g}]")

    for b in range(B):
        for g in range(Gmax):
            for gp2 in range(Gmax):
                if g == gp2: continue
                m.addConstr(S_grp[b, gp2] >= E_grp[b, g] - M * (1 - o_grp[b, g, gp2])
                            - M * (1 - v_grp[b, g]) - M * (1 - v_grp[b, gp2]),
                            name=f"C_grp_noovl1[{b},{g},{gp2}]")
                m.addConstr(S_grp[b, g] >= E_grp[b, gp2] - M * (o_grp[b, g, gp2])
                            - M * (1 - v_grp[b, g]) - M * (1 - v_grp[b, gp2]),
                            name=f"C_grp_noovl2[{b},{g},{gp2}]")

    for d in range(Dv):
        for b, g, bp, gp2 in itertools.product(range(B), range(Gmax), range(B), range(Gmax)):
            if (b, g) >= (bp, gp2): continue
            m.addConstr(S_grp[bp, gp2] >= E_grp[b, g] - M * (1 - o_dev[b, g, bp, gp2, d])
                        - M * (1 - y[b, g, d]) - M * (1 - y[bp, gp2, d]),
                        name=f"C_dev_noovl1[{b},{g},{bp},{gp2},{d}]")
            m.addConstr(S_grp[b, g] >= E_grp[bp, gp2] - M * (o_dev[b, g, bp, gp2, d])
                        - M * (1 - y[b, g, d]) - M * (1 - y[bp, gp2, d]),
                        name=f"C_dev_noovl2[{b},{g},{bp},{gp2},{d}]")

    # --- 完工与目标 ---
    for b in range(B):
        m.addConstr(C_b[b] >= E_shake[b], name=f"C_cb_after_shake[{b}]")
        for g in range(Gmax):
            m.addConstr(C_b[b] >= E_grp[b, g], name=f"C_cb_after_grp[{b},{g}]")
        m.addConstr(MKS >= C_b[b], name=f"C_makespan_def[{b}]")

    m.setObjective(MKS, GRB.MINIMIZE)
    m.Params.TimeLimit = time_limit
    m.Params.MIPGap = mip_gap
    # m.Params.OutputFlag = 0

    m.optimize()

    sol = {}
    if m.status in [GRB.OPTIMAL, GRB.TIME_LIMIT, GRB.SUBOPTIMAL]:
        sol['obj'] = m.objVal if m.SolCount > 0 else None
        sol['status'] = m.status
        sol['S_clean'] = {b: S_clean[b].X for b in range(B)}
        sol['E_clean'] = {b: E_clean[b].X for b in range(B)}
        sol['S_fill'] = {(b, c): S_fill[b, c].X for b in range(B) for c in range(C)}
        sol['E_fill'] = {(b, c): E_fill[b, c].X for b in range(B) for c in range(C)}
        sol['S_shake'] = {b: S_shake[b].X for b in range(B)}
        sol['E_shake'] = {b: E_shake[b].X for b in range(B)}
        sol['S_grp'] = {(b, g): S_grp[b, g].X for b in range(B) for g in range(Gmax)}
        sol['E_grp'] = {(b, g): E_grp[b, g].X for b in range(B) for g in range(Gmax)}
        sol['T_grp'] = {(b, g): T_grp[b, g].X for b in range(B) for g in range(Gmax)}
        sol['C_b'] = {b: C_b[b].X for b in range(B)}
        sol['MKS'] = MKS.X
        sol['z'] = {(b, c, g): int(round(z[b, c, g].X)) for b in range(B) for c in range(C-1) for g in range(Gmax)}
        sol['x'] = {(b, c, g, d): int(round(x[b, c, g, d].X)) for b in range(B) for c in range(C-1) for g in range(Gmax) for d in range(Dv)}
        sol['y'] = {(b, g, d): int(round(y[b, g, d].X)) for b in range(B) for g in range(Gmax) for d in range(Dv)}
    else:
        sol['status'] = m.status
        sol['obj'] = None

    return m, sol


# ------------------------- 结果打印 & 甘特图 -------------------------
def pretty_print_solution(inst: Dict, sol: Dict):
    B, C, Dv, Gmax = inst['B'], inst['C'], inst['D'], inst['Gmax']
    components, bottom = inst['components'], inst['bottom']
    anal_time, can = inst['anal_time'], inst['can']

    print("\n================= 求解结果 =================")
    print(f"状态: {sol['status']}  目标(Makespan): {sol.get('MKS')}")
    print("--------------------------------------------")
    for b in range(B):
        print(f"[瓶 {b}] 清洗: {sol['S_clean'][b]:.1f} → {sol['E_clean'][b]:.1f}")
        pairs = [((b, c), sol['S_fill'][b, c], sol['E_fill'][b, c]) for c in range(C)]
        pairs.sort(key=lambda t: t[1])
        for (bb, c), s, e in pairs:
            tag = " (底气)" if c == bottom else ""
            print(f"    充装 组分 {components[c]}{tag}: {s:.1f} → {e:.1f}")
        print(f"    摇匀: {sol['S_shake'][b]:.1f} → {sol['E_shake'][b]:.1f}")
        grp_list = [((b, g), sol['S_grp'][b, g], sol['E_grp'][b, g], sol['T_grp'][b, g]) for g in range(Gmax)]
        grp_list = [t for t in grp_list if t[1] + t[2] + t[3] > 1e-6]
        grp_list.sort(key=lambda t: t[1])
        for (bb, g), s, e, T in grp_list:
            print(f"    分析 组 g={g}: {s:.1f} → {e:.1f}  (T={T:.1f})")
            for c in range(C-1):
                for d in range(Dv):
                    if sol['x'][b, c, g, d] == 1:
                        print(f"        组分 {components[c]} → 设备 d={d}  (t={anal_time[d][c]}; 可用={can[d][c]})")
        print(f"    完工 C_b={sol['C_b'][b]:.1f}")
        print("--------------------------------------------")
    print(f"总完工时间 Makespan = {sol.get('MKS')}")
    print("============================================\n")


def draw_gantt(inst: Dict, sol: Dict, out_png: str = None):
    """
    绘制甘特图（单图）：每个瓶子一条横线，依次标注 清洗 / 充装(各组分) / 摇匀 / 分析组。
    注意：使用 matplotlib；不设置任何特定颜色；每个图只有一个轴（不做子图）。
    """
    import matplotlib.pyplot as plt

    B, C, Dv, Gmax = inst['B'], inst['C'], inst['D'], inst['Gmax']
    components, bottom = inst['components'], inst['bottom']

    rows = []
    tmax = 0.0
    eps = 1e-6

    for b in range(B):
        # 清洗
        s = sol['S_clean'][b]; e = sol['E_clean'][b]
        if e - s > eps:
            rows.append((b, s, e, '清洗'))
            tmax = max(tmax, e)

        # 充装（各组分）
        for c in range(C):
            s = sol['S_fill'][b, c]; e = sol['E_fill'][b, c]
            if e - s > eps:
                lab = f'充装 {components[c]}' + ('(底气)' if c == bottom else '')
                rows.append((b, s, e, lab))
                tmax = max(tmax, e)

        # 摇匀
        s = sol['S_shake'][b]; e = sol['E_shake'][b]
        if e - s > eps:
            rows.append((b, s, e, '摇匀'))
            tmax = max(tmax, e)

        # 分析组
        for g in range(Gmax):
            s = sol['S_grp'][b, g]; e = sol['E_grp'][b, g]; T = sol['T_grp'][b, g]
            if e - s > eps and T > eps:
                pairs = []
                for c in range(C-1):  # 不含底气
                    for d in range(Dv):
                        if sol['x'][b, c, g, d] == 1:
                            pairs.append(f"{components[c]}→d{d}")
                lab = '分析 g={}: {}'.format(g, ','.join(pairs) if pairs else '')
                rows.append((b, s, e, lab))
                tmax = max(tmax, e)

    if not rows:
        print("没有可绘制的任务区间。")
        return None

    fig, ax = plt.subplots(figsize=(12, 0.6 * max(4, B)))
    for (y, s, e, lab) in rows:
        ax.barh(y, e - s, left=s)  # 不指定颜色/样式
        ax.text(s + (e - s) / 2.0, y, lab, va='center', ha='center', fontsize=8)

    ax.set_xlabel('时间')
    ax.set_ylabel('瓶编号')
    ax.set_yticks(list(range(B)))
    ax.set_yticklabels([f'瓶 {b}' for b in range(B)])
    ax.set_xlim(0, tmax * 1.05)
    ax.invert_yaxis()
    ax.set_title('标气车间调度甘特图（Gurobi解）')

    if out_png is not None:
        import os
        os.makedirs(os.path.dirname(out_png), exist_ok=True)
        fig.savefig(out_png, bbox_inches='tight', dpi=150)
        print(f"Gantt 已保存：{out_png}")
    return fig


# ------------------------- 参数解析 & 主入口 -------------------------
def parse_args():
    ap = argparse.ArgumentParser()
    ap.add_argument('--preset', type=str, choices=['small','medium','large'], help='选择内置算例')
    ap.add_argument('--json', type=str, help='从 JSON 文件加载算例')
    ap.add_argument('--time_limit', type=int, default=60, help='求解时间上限(秒)')
    ap.add_argument('--mip_gap', type=float, default=1e-4, help='MIPGap 收敛阈值')
    ap.add_argument('--seed', type=int, default=42, help='随机种子（用于预设算例的随机生成）')
    return ap.parse_args()


def main():
    args = parse_args()
    set_seed(args.seed)

    # 选择算例
    if args.json:
        inst = load_instance_from_json(args.json)
        print(f"已从 JSON 加载：{args.json}")
    elif args.preset:
        inst = build_preset_instance(args.preset, seed=args.seed)
        print(f"使用预设：{args.preset}")
    else:
        # 交互式菜单
        print("请选择算例来源：\n  1) small 预设\n  2) medium 预设\n  3) large 预设\n  4) 从 JSON 加载")
        choice = input("输入选项 [1/2/3/4]: ").strip()
        if choice == '1':
            inst = build_preset_instance('small', seed=args.seed); print("使用 small 预设")
        elif choice == '2':
            inst = build_preset_instance('medium', seed=args.seed); print("使用 medium 预设")
        elif choice == '3':
            inst = build_preset_instance('large', seed=args.seed); print("使用 large 预设（可能较慢）")
        elif choice == '4':
            path = input("请输入 JSON 路径: ").strip()
            inst = load_instance_from_json(path); print(f"已从 JSON 加载：{path}")
        else:
            print("无效选项，默认使用 small 预设")
            inst = build_preset_instance('small', seed=args.seed)

    # 求解
    model, sol = build_and_solve(inst, time_limit=args.time_limit, mip_gap=args.mip_gap)

    # 打印与绘图
    if sol.get('obj') is not None:
        pretty_print_solution(inst, sol)
        

        # 自动保存甘特图
        import os
        tag = 'preset' if getattr(args, 'preset', None) else 'json'
        name = (args.preset if tag=='preset' else (os.path.splitext(os.path.basename(args.json))[0] if getattr(args, 'json', None) else 'interactive'))
        out_png = os.path.join('plots', f'gantt_{name}.png')
        draw_gantt(inst, sol, out_png=out_png)
        print(f"(提示) 甘特图文件: {out_png}")
    else:
        print("未获得可用解，状态码：", sol['status'])


if __name__ == "__main__":
    main()
